/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.virtual.model.sax;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeDefinition;
import offset.nodes.Constants;
import offset.nodes.client.model.StringArrayConverter;
import offset.nodes.client.virtual.model.jcr.QName;
import offset.nodes.client.virtual.model.jcr.SimpleNode;
import offset.nodes.server.html.model.NodeSubstituter;
import offset.nodes.server.virtual.model.VirtualSubstituter;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 *
 * @author Walter Luetgenau
 */
public abstract class AbstractContentSource implements SAXContentSource {
    ContentHandler handler;
    Stack<String> path;
    String pathString;
    Stack<String> rootPath;
    Attributes empty = new AttributesImpl();
    List<NodeSubstituter> nodeSubstituters = new ArrayList<NodeSubstituter>();
    Session session;

    public AbstractContentSource(Session session) {
        this.session = session;
        initSubstituters();
    }

    public void setContentHandler(ContentHandler handler) {
        this.handler = handler;
    }

    public List<NodeSubstituter> getNodeSubstituters() {
        return nodeSubstituters;
    }

    protected Stack createPath(String path) {
        StringTokenizer parser = new StringTokenizer(path, "/");
        Stack newPath = new Stack();

        while (parser.hasMoreTokens()) {
            String pathComponent = parser.nextToken();
            newPath.push(pathComponent);
        }

        return newPath;
    }

    public Stack<String> getPath() {
        return path;
    }

    public void setPath(String path) {
        this.pathString = path;
        this.path = createPath(path);
        this.rootPath = (Stack<String>) this.path.clone();
    }

    protected String normalizeName(String name) {
        int found = name.indexOf("[");
        if (found >= 0) {
            return name.substring(0, found);
        }
        return name;
    }

    protected void addAttribute(AttributesImpl atts, String name, String value) {
        name = normalizeName(name);
        
        QNameToSAX converter = new QNameToSAX(name);
        atts.addAttribute(converter.getUri(), converter.getLocalName(), converter.getQName(), "CDATA", value);
    }

    protected void startElement(String name) throws SAXException {
        startElement(name, empty);
    }

    protected void startElement(String name, Attributes atts) throws SAXException {
        name = normalizeName(name);
        
        QNameToSAX converter = new QNameToSAX(name);
        handler.startElement(converter.getUri(), converter.getLocalName(), converter.getQName(), atts);
    }

    protected void endElement(String name) throws SAXException {
        name = normalizeName(name);
        
        QNameToSAX converter = new QNameToSAX(name);
        handler.endElement(converter.getUri(), converter.getLocalName(), converter.getQName());
    }
    
    protected String formatValue(Value value) throws RepositoryException {
        if (value.getType() == PropertyType.DATE) {
            return DateFormat.getDateTimeInstance().format(value.getDate());
        }
        
        return value.getString();
    }

    protected void updatePath(Stack<String> newPath, Node node) throws SAXException, RepositoryException {
        int depth;
        for (depth = 0; depth < path.size() && depth < newPath.size(); depth++) {
            if (!path.get(depth).equals(newPath.get(depth))) {
                break;
            }
        }
        for (int i = path.size() - 1; i >= depth; i--) {
            endElement((String) path.pop());
        }
        if (depth == path.size() && depth == newPath.size() ) // both pathes are equal. Terminate the previous element, then
            endElement((String) path.pop());

        PropertyIterator it = node.getProperties();
        AttributesImpl atts = new AttributesImpl();
        while (it.hasNext()) {
            Property property = it.nextProperty();
            if (!property.getDefinition().isMultiple() ) {
                addAttribute(atts, property.getName(), property.getString());                   
            }
            else {
                StringArrayConverter converter = new StringArrayConverter(property.getValues());
                addAttribute(atts, property.getName(), converter.toString());
            }
        }

        for (int i = depth; i < newPath.size() - 1; i++) {
            startElement((String) path.push(newPath.get(i)));
        }
        startElement((String) path.push(newPath.get(newPath.size() - 1)), atts);
 
    }

    protected void terminatePath() throws SAXException, RepositoryException {
        while (path.size() > rootPath.size()) {
            String name = normalizeName((String) path.pop());
            endElement(name);
        }
    }

    /**
     * Modify the node, if necessary, before feeding it into the XSL transformation.
     * As this may include path and name transformation, the parent node must be provided.
     *
     * @param node
     * @param parent
     * @throws javax.jcr.RepositoryException
     * @return the modified node
     */
    protected Node substituteNode(Node node, Node parent) throws RepositoryException {
        Iterator<NodeSubstituter> i = getNodeSubstituters().iterator();

        while (i.hasNext()) {
            NodeSubstituter substituter = i.next();
            node = substituter.substitute(node, parent);
        }

        return node;
    }

     /**
     * Write a node
     *
     * @param node
     * @param parent
     * @throws RepositoryException
     * @throws SAXException
     */
    protected void writeNode(Node node, Node parent) throws RepositoryException, SAXException {
        Node substitutedNode = substituteNode(node, parent);
        updatePath(createPath(substitutedNode.getPath()), substitutedNode);
        // use the original node for the children, as the SimpleNode does not have all necessary methods
         if (!topAlreadyInPath()) { // for now, use only the immediate children
            NodeIterator i = node.getNodes();
            while (i.hasNext()) {
                writeNode(i.nextNode(), substitutedNode);
            }
        }
    }

    /**
     * Is the top element of the path already present on some other level?
     * 
     * @return
     */
    protected boolean topAlreadyInPath() {
        String top = path.peek();

        for (int i = rootPath.size(); i < path.size() - 2; i++)
            if (path.get(i).equals(top))
                return true;

        return false;
    }

    /**
     * Write a node
     * 
     * @param node
     * @throws RepositoryException
     * @throws SAXException
     */
    protected void writeNode(Node node) throws RepositoryException, SAXException {
        writeNode(node, null);
    }

    public void initSubstituters() {
        getNodeSubstituters().add(new ResidualNameSubstituter());
        getNodeSubstituters().add(new VirtualSubstituter(session));
    }

    class ResidualNameSubstituter implements NodeSubstituter {

        @Override
        public Node substitute(Node node, Node parent) throws RepositoryException {
            SimpleNode result;
            if (node instanceof SimpleNode)
                result = (SimpleNode) node;
            else
                result = new SimpleNode(node, SimpleNode.FORMAT_USER, SimpleNode.COPYMODE_DO_NOT_COPY_CHILDREN);
            if (hasResidualName(node, parent)) {
                String typeName = result.getNodeType();
                if (parent != null && !(node instanceof SimpleNode) )
                    typeName = useTypeFromParentChildDefinition(node, typeName);
                // use a simple node to exchange the properties. Use user display format.
                result.setProperty(QName.NAME_PROPERTY.getLocalName(), node.getName());
                result.setName(typeName);
            }
            if (parent != null)
                result.setPath(parent.getPath() + "/" + result.getName());
            else
                result.setPath(pathString + Constants.PATH_SEPARATOR + result.getName());
            
            return result;
        }

        /**
         * The transformation will only know the type names of the child node definition. For a node type
         * extending that definition, the type name of the child definition must be used to make it "visible"
         * to the transformation.
         *
         * @param node
         * @param nodeType
         * @return
         * @throws RepositoryException
         */
        protected String useTypeFromParentChildDefinition(Node node, String nodeType) throws RepositoryException {
            Node parent = node.getParent();

            for (NodeDefinition child : parent.getPrimaryNodeType().getChildNodeDefinitions())
                if (node.isNodeType(child.getDefaultPrimaryType().getName()))
                    return child.getDefaultPrimaryType().getName();

            return nodeType;
        }

       /**
         * Check, if this is a node with a residual definition
         *
         * @param node The node to be checked
         * @return The result of the check
         * @throws javax.jcr.RepositoryException
         */
        protected boolean hasResidualName(Node node, Node parent) throws RepositoryException {
            if (parent == null)
                return true; // top level nodes must use the type as a name always
            String name = node.getDefinition().getName();
            return QName.ANY_NAME.getLocalName().equals(node.getDefinition().getName());
        }
    }


}
